Skip to content

Conversation

@nbalkandzhiyski
Copy link
Contributor

@nbalkandzhiyski nbalkandzhiyski commented Jan 6, 2026

This new feature adds a additional pre-processing step in app/Actions/Photo/Create.php ** $pre_pipes** to detect HEIF file types and convert them to jpeg in order to support working with these file types.

Also during our work we found that the photos.title is varchar(100) but our test images from IPhone 15+ had longer titles, so we had to add a migration to change the title to text column in order to support longer titles.
Ability to add more convertible file types in app/Actions/Photo/Convert/ImageTypeFactory.php

Added fallback image conversion via Go https://github.com/MaestroError/php-heic-to-jpg#php-heic-to-jpg because ImageMagick fails to convert some HEIF images, mostly from IPhone 15+ devices

Key points:
• Pre-processing happens early in the upload/import lifecycle, before AssertSupportedMedia.
• Conversion uses existing ImageMagick support when available (no new hard dependency).
• Converted file transparently replaces the original HEIC/HEIF file for downstream processing.
• Original file remains untouched if conversion fails.
• Fully compatible with S3 / remote storage setups, as conversion happens while the file is still local/temporary.
• Designed as an extensible pre-processing stage, not a HEIC-only hack.

Summary by CodeRabbit

  • New Features

    • Added support for HEIF/HEIC uploads with automatic conversion to JPEG during photo import.
    • Upload pipeline now normalizes unsupported image formats before validation.
  • Database

    • Expanded photo title length and updated related indexes to support longer titles.
  • Tests

    • Added unit and integration tests covering HEIF/HEIC upload and conversion flows.

✏️ Tip: You can customize this high-level summary in your review settings.

- Introduced `HeifToJpeg` class for converting HEIC/HEIF images to JPEG format.
- Added `ImageTypeFactory` to determine conversion classes based on file extensions.
- Implemented `ConvertUnsupportedMedia` pipe to handle unsupported media conversions.
- Created `ConvertMediaFileInterface` for consistent conversion method signatures.
- Added `CannotConvertMediaFileException` for error handling during conversion.
- Updated `Create` action to include the new conversion pipe.
- Introduced `ConvertableImageType` enum to manage image type checks.
- Updated variable names to use snake case  in `HeifToJpeg` and `ConvertMediaFileInterface` for consistency and clarity.
…ecause titles over 100 chars throws error when uploading images
…000) in photos table for improved title length handling
…o TEXT in photos table, updating index to accommodate new column type and ensuring proper handling of long titles.
…existence check before dropping it, ensuring smoother migration process.
…o-jpg

- Updated `composer.json` and `composer.lock` to include the new dependency.
- Enhanced `HeifToJpeg` class to utilize the `HeicToJpg` library for conversion.
- Modified methods to handle both Imagick and HeicToJpg instances for image processing.
@nbalkandzhiyski nbalkandzhiyski requested a review from a team as a code owner January 6, 2026 14:04
@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Adds HEIC/HEIF support via a conversion pipeline: enum and service updates for HEIF types, a PhotoConverter interface and factory, a HeifToJpeg Imagick converter, a ConvertUnsupportedMedia init pipe integrated into photo creation, tests, and a migration adjusting photos.title and related indexes.

Changes

Cohort / File(s) Summary
Image Format Types & Errors
app/Enum/ConvertableImageType.php, app/Exceptions/CannotConvertMediaFileException.php
New enum for HEIC/HEIF detection and a dedicated CannotConvertMediaFileException (HTTP 422).
Conversion Interface & Factory
app/Contracts/PhotoCreate/PhotoConverter.php, app/Actions/Photo/Convert/PhotoConverterFactory.php
Added PhotoConverter interface and factory to resolve converters by extension.
HEIF → JPEG Converter
app/Actions/Photo/Convert/HeifToJpeg.php
New HeifToJpeg implementing PhotoConverter using Imagick to produce JPEG (quality 92, auto-orient); deletes original on success and wraps Imagick errors.
Photo Create Pipeline
app/Actions/Photo/Create.php, app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
Inserted ConvertUnsupportedMedia pipe into pre_pipes before AssertSupportedMedia; pipe detects convertible types and replaces state->source_file with converted TemporaryJobFile.
Image Extension/MIME Support
app/Services/Image/FileExtensionService.php
Added HEIC/HEIF to supported EXIF image types, file extensions, and MIME type mappings.
Tests & Test Constants
tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php, tests/Constants/TestConstants.php, tests/ImageProcessing/Photo/PhotoAddTest.php
New unit tests for ConvertUnsupportedMedia (pass-through, conversion, error wrapping), test constants for HEIC/HEIF, and integration tests testHeicPicture/testHeifPicture.
Database Migration
database/migrations/2026_01_14_105523_change_title_to_text_on_photos_table.php
Migration changes photos.title length/indexes (string(100) → string(300) and new composite index); includes rollback.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through bytes and formats new,
HEIC and HEIF I nibbled through,
Imagick twirled, pixels set free,
Pipes aligned—now we both agree.
A carrot-sized commit—hoppy and true!

🚥 Pre-merge checks | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.



📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7b83f6 and 9b5c877.

📒 Files selected for processing (1)
  • database/migrations/2026_01_14_105523_change_title_to_text_on_photos_table.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • database/migrations/2026_01_14_105523_change_title_to_text_on_photos_table.php
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: 2️⃣ PHP dist / 8.4 - postgresql
  • GitHub Check: 1️⃣ PHP tests / 8.4 - postgresql -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Webshop
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Precomputing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- ImageProcessing
  • GitHub Check: 1️⃣ PHP tests / 8.5 - sqlite -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Unit
  • GitHub Check: 1️⃣ PHP tests / 8.5 - postgresql -- Feature_v2
  • GitHub Check: 1️⃣ PHP tests / 8.4 - mariadb -- Install
  • GitHub Check: 1️⃣ PHP tests / 8.5 - mariadb -- Unit

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (2)
app/Actions/Photo/Convert/ImageTypeFactory.php (1)

15-24: Consider making the property readonly and using FQCN.

The public mutable property $convertionClass (after fixing the typo to $conversionClass) could be made readonly since it's only set in the constructor. Additionally, storing the fully-qualified class name (FQCN) instead of just the class name would eliminate fragile string concatenation in the make() method.

🔎 Suggested refactor
-	public ?string $convertionClass = null;
+	public readonly ?string $converterClass;
 
 	public function __construct(string $extension)
 	{
-		$this->convertionClass = match (true) {
-			ConvertableImageType::isHeifImageType($extension) => 'HeifToJpeg',
+		$this->converterClass = match (true) {
+			ConvertableImageType::isHeifImageType($extension) => HeifToJpeg::class,
 			// TODO: Add more convertion types/classes
 			default => null,
 		};
 	}

Then simplify the make() method:

 	public function make(): ConvertMediaFileInterface
 	{
-		$class = 'App\Actions\Photo\Convert\\' . $this->convertionClass;
+		if ($this->converterClass === null) {
+			throw new \RuntimeException('No conversion class available for this file type');
+		}
+
+		$instance = new ($this->converterClass)();
 
-		return new $class();
+		if (!$instance instanceof ConvertMediaFileInterface) {
+			throw new \RuntimeException("Converter must implement ConvertMediaFileInterface");
+		}
+
+		return $instance;
 	}
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (1)

27-31: Typo: "convertionClass" should be "conversionClass".

The property name in ImageTypeFactory contains a spelling error. While consistent across files, correcting it would improve code readability.

This would require updating ImageTypeFactory.php as well:

-		if ($factory->convertionClass === null) {
+		if ($factory->conversionClass === null) {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e5f7b1 and 25cfca0.

📒 Files selected for processing (10)
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Actions/Photo/Create.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Contracts/Image/ConvertMediaFileInterface.php
  • app/Enum/ConvertableImageType.php
  • app/Enum/ImageType.php
  • app/Exceptions/CannotConvertMediaFileException.php
  • composer.json
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
🧰 Additional context used
📓 Path-based instructions (1)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • app/Exceptions/CannotConvertMediaFileException.php
  • app/Enum/ConvertableImageType.php
  • app/Enum/ImageType.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Contracts/Image/ConvertMediaFileInterface.php
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
  • app/Actions/Photo/Create.php
🧠 Learnings (5)
📓 Common learnings
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3626
File: database/migrations/2025_06_07_144157_photo_tags_to_table.php:87-105
Timestamp: 2025-08-18T10:19:04.946Z
Learning: In the Lychee photo management system, the migration `2025_06_07_144157_photo_tags_to_table.php` runs on data that only contains comma-separated tag names in tag_albums.show_tags - OR/AND expressions do not exist at this migration time, so handling them is unnecessary.
📚 Learning: 2025-08-25T08:48:12.321Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 2562
File: app/Exceptions/Internal/ZipExtractionException.php:9-17
Timestamp: 2025-08-25T08:48:12.321Z
Learning: In the Lychee codebase, LycheeDomainException exists in the App\Exceptions\Internal namespace. Exception classes within the same App\Exceptions\Internal namespace can extend LycheeDomainException without requiring explicit use statements, following standard PHP namespace resolution rules.

Applied to files:

  • app/Exceptions/CannotConvertMediaFileException.php
📚 Learning: 2025-08-25T19:07:01.275Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 2562
File: tests/Feature_v2/Photo/PhotoZipUploadTest.php:81-95
Timestamp: 2025-08-25T19:07:01.275Z
Learning: In the Lychee codebase, when testing ZIP file uploads in PhotoZipUploadTest, Laravel middleware does not trigger for ZipInvalidException, so the exception is thrown directly to PHPUnit rather than being converted to an HTTP response. This means using $this->expectException(ZipInvalidException::class) is the correct approach rather than asserting HTTP status codes.

Applied to files:

  • app/Exceptions/CannotConvertMediaFileException.php
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • app/Exceptions/CannotConvertMediaFileException.php
  • app/Enum/ConvertableImageType.php
  • app/Enum/ImageType.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Contracts/Image/ConvertMediaFileInterface.php
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
  • app/Actions/Photo/Create.php
📚 Learning: 2025-08-18T10:19:04.946Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3626
File: database/migrations/2025_06_07_144157_photo_tags_to_table.php:87-105
Timestamp: 2025-08-18T10:19:04.946Z
Learning: In the Lychee photo management system, the migration `2025_06_07_144157_photo_tags_to_table.php` runs on data that only contains comma-separated tag names in tag_albums.show_tags - OR/AND expressions do not exist at this migration time, so handling them is unnecessary.

Applied to files:

  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
🧬 Code graph analysis (4)
app/Enum/ConvertableImageType.php (1)
app/DTO/ImportEventReport.php (1)
  • str (93-96)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (5)
app/Actions/Photo/Convert/ImageTypeFactory.php (2)
  • ImageTypeFactory (13-32)
  • make (26-31)
app/DTO/PhotoCreate/InitDTO.php (1)
  • InitDTO (18-61)
app/Exceptions/CannotConvertMediaFileException.php (1)
  • CannotConvertMediaFileException (18-26)
app/Contracts/Image/ConvertMediaFileInterface.php (1)
  • handle (17-17)
app/Actions/Photo/Convert/HeifToJpeg.php (1)
  • handle (24-45)
app/Actions/Photo/Convert/ImageTypeFactory.php (1)
app/Enum/ConvertableImageType.php (1)
  • isHeifImageType (16-24)
app/Actions/Photo/Create.php (1)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (1)
  • ConvertUnsupportedMedia (16-41)
🔇 Additional comments (5)
app/Actions/Photo/Create.php (1)

82-88: LGTM! Proper placement in the pipeline.

The new ConvertUnsupportedMedia pipe is correctly positioned before AssertSupportedMedia, allowing unsupported formats to be converted before validation. The error handling in the pipe properly throws CannotConvertMediaFileException for conversion failures.

app/Enum/ImageType.php (1)

1-19: LGTM! Well-structured enum.

The enum correctly maps to PHP's native IMAGETYPE_* constants, providing type-safe image type representation. The implementation follows all coding guidelines including proper license headers and formatting.

app/Enum/ConvertableImageType.php (1)

1-25: LGTM!

The enum implementation is clean and follows best practices:

  • License header present with correct format
  • in_array() correctly uses true as the third parameter for strict comparison
  • The str()->lower()->toString() approach properly handles case-insensitive extension matching
app/Exceptions/CannotConvertMediaFileException.php (1)

1-26: LGTM!

The exception class is well-structured:

  • Appropriate HTTP 422 status for media conversion failures
  • Follows the existing BaseLycheeException pattern in the codebase
  • Clean constructor with optional message and previous exception chaining
app/Actions/Photo/Convert/HeifToJpeg.php (1)

80-103: Solid fallback strategy for HEIC/HEIF conversion.

The implementation correctly handles the known limitation where ImageMagick may fail on newer iPhone images by falling back to the php-heic-to-jpg library. The multi-frame handling and auto-orientation are good practices for preserving image quality.

The maestroerror/php-heic-to-jpg package is compatible with PHP 7.4 and above. Ensure the installed version is 1.0.5 or later to avoid the RCE vulnerability (CVE-2024-48514) present in earlier versions.

- Bump `maestroerror/php-heic-to-jpg` dependency version in `composer.json`.
- Refactor `HeifToJpeg` class to use filename instead of basename for output path.
- Enhance `ImageTypeFactory` to ensure conversion class validity and implement interface checks.
- Improve error handling in `ConvertUnsupportedMedia` by including the original exception in the thrown `CannotConvertMediaFileException`.
- Update migration for 'title' column in photos table to allow null values and adjust indexing accordingly.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (1)

27-31: Prefer encapsulated check over direct property access.

Accessing $factory->conversionClass directly exposes internal factory state and couples the pipe to implementation details. Consider adding a method to ImageTypeFactory such as canConvert(): bool or hasConverter(): bool for better encapsulation.

🔎 Example refactor in ImageTypeFactory

Add a method to ImageTypeFactory:

public function canConvert(): bool
{
    return $this->conversionClass !== null;
}

Then update this pipe:

 $factory = new ImageTypeFactory($ext);
 
-if ($factory->conversionClass === null) {
+if (!$factory->canConvert()) {
     return $next($state);
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25cfca0 and 584deef.

📒 Files selected for processing (5)
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • composer.json
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Actions/Photo/Convert/ImageTypeFactory.php
🧰 Additional context used
📓 Path-based instructions (1)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
🧠 Learnings (4)
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
📚 Learning: 2025-08-18T10:19:04.946Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3626
File: database/migrations/2025_06_07_144157_photo_tags_to_table.php:87-105
Timestamp: 2025-08-18T10:19:04.946Z
Learning: In the Lychee photo management system, the migration `2025_06_07_144157_photo_tags_to_table.php` runs on data that only contains comma-separated tag names in tag_albums.show_tags - OR/AND expressions do not exist at this migration time, so handling them is unnecessary.

Applied to files:

  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to **/*.php : For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).

Applied to files:

  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
📚 Learning: 2025-08-20T20:32:03.676Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3637
File: database/migrations/2025_08_19_160144_create_renamer_rules_table.php:106-108
Timestamp: 2025-08-20T20:32:03.676Z
Learning: In the Lychee codebase, ildyria intentionally uses destructive migration patterns where up() calls down() to ensure clean state, as indicated by "Working as intended" and "No mercy" comments in migrations like database/migrations/2025_08_19_160144_create_renamer_rules_table.php.

Applied to files:

  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
🧬 Code graph analysis (1)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (5)
app/Actions/Photo/Convert/ImageTypeFactory.php (2)
  • ImageTypeFactory (14-47)
  • make (27-46)
app/DTO/PhotoCreate/InitDTO.php (1)
  • InitDTO (18-61)
app/Exceptions/CannotConvertMediaFileException.php (1)
  • CannotConvertMediaFileException (18-26)
app/Actions/Photo/Convert/HeifToJpeg.php (1)
  • handle (24-45)
app/Contracts/Image/ConvertMediaFileInterface.php (1)
  • handle (17-17)
🔇 Additional comments (4)
composer.json (1)

71-71: LGTM! Dependency constraint addresses critical vulnerability.

The version constraint ^1.0.5 correctly excludes versions affected by CVE-2024-48514 (critical RCE vulnerability). Latest version v1.0.8 is available and will be installed.

app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (1)

33-37: LGTM! Exception chain properly preserved.

The conversion logic correctly wraps exceptions in CannotConvertMediaFileException while preserving the original exception chain for debugging.

app/Actions/Photo/Convert/HeifToJpeg.php (2)

24-45: LGTM! Conversion workflow is well-structured.

The handle method implements a clean workflow: convert → store → delete → return. Exception handling correctly preserves the original error message for debugging.


80-103: LGTM! Robust conversion with appropriate fallback.

The conversion method correctly:

  • Handles multi-frame images by selecting the first frame
  • Applies appropriate JPEG settings (quality 92, auto-orient)
  • Falls back to HeicToJpg library when Imagick fails
  • Preserves error messages from both conversion attempts (line 100)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (1)

18-22: Update docblock to reflect actual exception type.

The docblock specifies @throws \Exception but the method actually throws CannotConvertMediaFileException (line 36), which is more specific and informative.

🔎 Proposed fix
 	/**
 	 * Tries to convert the file to a supported format.
 	 *
-	 * @throws \Exception
+	 * @throws CannotConvertMediaFileException
 	 */
app/Actions/Photo/Convert/ImageTypeFactory.php (1)

16-16: Consider making the property readonly.

Since $conversionClass is set in the constructor and never modified afterward, declaring it as readonly would better express its immutability.

🔎 Proposed change
-	public ?string $conversionClass = null;
+	public readonly ?string $conversionClass;

Note: You'll also need to adjust the constructor to set it via parameter or assignment, as readonly properties must be initialized during construction.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 584deef and f8f91e4.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Contracts/Image/ConvertMediaFileInterface.php
  • app/Enum/ConvertableImageType.php
  • app/Enum/ImageType.php
  • app/Exceptions/CannotConvertMediaFileException.php
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/Contracts/Image/ConvertMediaFileInterface.php
  • app/Exceptions/CannotConvertMediaFileException.php
  • app/Actions/Photo/Convert/HeifToJpeg.php
  • database/migrations/2025_12_22_105523_change_title_to_text_on_photos_table.php
  • app/Enum/ConvertableImageType.php
🧰 Additional context used
📓 Path-based instructions (1)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Enum/ImageType.php
  • app/Actions/Photo/Convert/ImageTypeFactory.php
🧠 Learnings (1)
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • app/Enum/ImageType.php
  • app/Actions/Photo/Convert/ImageTypeFactory.php
🧬 Code graph analysis (2)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (5)
app/Actions/Photo/Convert/ImageTypeFactory.php (2)
  • ImageTypeFactory (14-47)
  • make (27-46)
app/DTO/PhotoCreate/InitDTO.php (1)
  • InitDTO (18-61)
app/Exceptions/CannotConvertMediaFileException.php (1)
  • CannotConvertMediaFileException (18-26)
app/Actions/Photo/Convert/HeifToJpeg.php (1)
  • handle (24-45)
app/Contracts/Image/ConvertMediaFileInterface.php (1)
  • handle (17-17)
app/Actions/Photo/Convert/ImageTypeFactory.php (1)
app/Enum/ConvertableImageType.php (1)
  • isHeifImageType (16-24)
🔇 Additional comments (2)
app/Enum/ImageType.php (1)

11-18: LGTM! Clean enum implementation.

The enum correctly maps to PHP's built-in IMAGETYPE_* constants and provides a type-safe way to represent supported image types.

app/Actions/Photo/Convert/ImageTypeFactory.php (1)

27-46: Write tests for ImageTypeFactory following test-first development practices.

No tests currently exist for ImageTypeFactory. Per coding guidelines requiring test-driven development, add unit tests covering:

  • HEIF/HEIC extension identification and converter instantiation
  • Exception handling for null conversion class (unsupported extensions)
  • Exception handling for missing converter class
  • Exception handling for invalid ConvertMediaFileInterface implementation

…ror handling and testing

- Change `conversionClass` property in `ImageTypeFactory` to be readonly.
- Update exception type in `ConvertUnsupportedMedia` to `CannotConvertMediaFileException`.
- Add unit tests for `ConvertUnsupportedMedia` to ensure proper handling of various image formats and conversion scenarios.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php (2)

46-116: Consider using a data provider to reduce duplication in passthrough tests.

The three passthrough tests (testHandleWithJpegExtensionPassesThrough, testHandleWithPngExtensionPassesThrough, testHandleWithExtensionWithoutDotPassesThrough) have nearly identical logic with only the extension value differing.

🔎 Proposed refactor using data provider
+	/**
+	 * @dataProvider passthroughExtensionsProvider
+	 */
+	public function testHandleWithNonConvertibleExtensionPassesThrough(string $extension): void
+	{
+		$originalFile = \Mockery::mock(NativeLocalFile::class);
+
+		$originalFile->shouldReceive('getOriginalExtension')
+			->once()
+			->andReturn($extension);
+
+		$state = $this->createInitDTO($originalFile);
+
+		$nextCalled = false;
+		$next = function (InitDTO $state) use (&$nextCalled, $originalFile): InitDTO {
+			$nextCalled = true;
+			self::assertSame($originalFile, $state->source_file);
+
+			return $state;
+		};
+
+		$result = $this->pipe->handle($state, $next);
+
+		$this->assertTrue($nextCalled);
+		$this->assertSame($originalFile, $result->source_file);
+	}
+
+	public static function passthroughExtensionsProvider(): array
+	{
+		return [
+			'jpeg with dot' => ['.jpg'],
+			'png with dot' => ['.png'],
+			'jpeg without dot' => ['jpg'],
+		];
+	}

118-149: Unused mock variable on line 121.

The $convertedFile mock is created but never used in the test. This appears to be leftover code from a different test approach.

🔎 Proposed fix
 	public function testHandleWithHeifExtensionTriggersConversion(): void
 	{
 		$originalFile = \Mockery::mock(NativeLocalFile::class);
-		$convertedFile = \Mockery::mock(TemporaryJobFile::class);
 
 		$originalFile->shouldReceive('getOriginalExtension')
app/Actions/Photo/Convert/ImageTypeFactory.php (1)

22-22: Fix typo in comment: "convertion" → "conversion".

🔎 Proposed fix
-			// TODO: Add more convertion types/classes
+			// TODO: Add more conversion types/classes
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8f91e4 and c2d5a1f.

📒 Files selected for processing (3)
  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.php: Any new PHP file should contain the license header and have a single blank line after the opening PHP tag
Variable names should be in snake_case in PHP
Apply the PSR-4 coding standard in PHP
Use in_array() with true as the third parameter in PHP
Only use booleans in if statements, not integers or strings
Use strict comparison (===) instead of loose comparison (==)
Avoid code duplication in both if and else statements
Do not use empty() in PHP
Use the moneyphp/money library for handling monetary values in PHP
Never use floats or doubles to represent monetary values; use integers representing the smallest currency unit (e.g., cents for USD)

**/*.php: Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.
For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).
Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.
Spotless now uses Palantir Java Format 2.78.0 with a 120-character wrap; configure IDE formatters to match before pushing code changes.
Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.
When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target (for example phpstan) before the full pipeline. Note the command in the related plan/task.
For PHP changes, ru...

Files:

  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php
tests/Unit/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Tests in the tests/Unit directory should extend from AbstractTestCase

Files:

  • tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php
tests/**/*.php

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

No need to mock the database in tests; use the in-memory SQLite database instead

Files:

  • tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php
🧠 Learnings (6)
📚 Learning: 2025-12-28T18:12:55.752Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 3901
File: app/Providers/AppServiceProvider.php:0-0
Timestamp: 2025-12-28T18:12:55.752Z
Learning: When using Laravel Octane's tick API, Octane::tick(...) returns an InvokeTickCallable that only has ->seconds(int) and ->immediate() methods. There is no ->every(N) method. Use the correct usage: Octane::tick('name', fn() => ...)->seconds(N) or Octane::tick('name', fn() => ..., N). Apply this guideline to PHP files across the project (not just AppServiceProvider.php).

Applied to files:

  • app/Actions/Photo/Convert/ImageTypeFactory.php
  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to **/*.php : Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring. List the expected success, validation, and failure branches and add thin failing tests for each path.

Applied to files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to **/*.php : Always run phpunit tests. If a test remains red, disable it with a TODO, note the reason, and capture the follow-up in the relevant plan.

Applied to files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
📚 Learning: 2025-08-25T19:07:01.275Z
Learnt from: ildyria
Repo: LycheeOrg/Lychee PR: 2562
File: tests/Feature_v2/Photo/PhotoZipUploadTest.php:81-95
Timestamp: 2025-08-25T19:07:01.275Z
Learning: In the Lychee codebase, when testing ZIP file uploads in PhotoZipUploadTest, Laravel middleware does not trigger for ZipInvalidException, so the exception is thrown directly to PHPUnit rather than being converted to an HTTP response. This means using $this->expectException(ZipInvalidException::class) is the correct approach rather than asserting HTTP status codes.

Applied to files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
  • tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to **/*.php : For PHP code, adhere to conventions: license headers in new files, strict comparison (===), no empty(), in_array() with third parameter true, snake_case variables, PSR-4 standard, test base classes (AbstractTestCase for Unit, BaseApiWithDataTest for Feature_v2).

Applied to files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
📚 Learning: 2025-12-22T14:12:18.082Z
Learnt from: CR
Repo: LycheeOrg/Lychee PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T14:12:18.082Z
Learning: Applies to **/*.php : For PHP changes, run php artisan test to ensure all tests pass before committing.

Applied to files:

  • app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php
🧬 Code graph analysis (2)
app/Actions/Photo/Convert/ImageTypeFactory.php (1)
app/Enum/ConvertableImageType.php (1)
  • isHeifImageType (16-24)
tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php (6)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (2)
  • ConvertUnsupportedMedia (16-41)
  • handle (23-40)
app/DTO/ImportMode.php (1)
  • ImportMode (14-41)
app/DTO/ImportParam.php (1)
  • ImportParam (14-33)
app/DTO/PhotoCreate/InitDTO.php (1)
  • InitDTO (18-61)
app/Exceptions/CannotConvertMediaFileException.php (1)
  • CannotConvertMediaFileException (18-26)
app/Image/Files/TemporaryJobFile.php (1)
  • TemporaryJobFile (20-76)
🔇 Additional comments (4)
app/Actions/Photo/Pipes/Init/ConvertUnsupportedMedia.php (1)

1-41: LGTM! Clean implementation of the conversion pipe.

The implementation correctly:

  • Preserves the exception chain by passing the original exception as the second parameter to CannotConvertMediaFileException
  • Uses strict comparison (===) for null check
  • Handles extensions with or without leading dots via ltrim()
  • Follows the single responsibility principle by delegating conversion to the factory
tests/Unit/Actions/Photo/Pipes/Init/ConvertUnsupportedMediaTest.php (2)

183-243: Good coverage of exception wrapping behavior.

The tests properly verify that:

  1. The original exception is preserved via getPrevious()
  2. The wrapper message includes context about the conversion failure
  3. Both base Exception and RuntimeException are handled consistently

245-257: Clean helper method for test setup.

The createInitDTO helper properly encapsulates the DTO construction, making tests more readable and maintainable.

app/Actions/Photo/Convert/ImageTypeFactory.php (1)

27-46: Well-structured factory method with proper guards.

The make() method includes all necessary validations:

  • Null check before attempting instantiation
  • Class existence verification
  • Interface implementation check

This defensive approach prevents runtime errors from bubbling up unexpectedly.

@ildyria
Copy link
Member

ildyria commented Jan 7, 2026

Hi,

Thank you for your pull request. I really agree that we need to have a pre-processing step that can be further extended.

However, I have a few issues that needs to be resolved:

  • the vulnerabilities in the golang binaries see the findings from trivy. Those are simply not acceptable.
  • Update go.mod MaestroError/php-heic-to-jpg#37 < this update does not inspire me trust in the maintaining dev.
    This has been dragging for more than 1.5 year, we are now on go 1.25, this should be kept up to date.

Looking more into details:

So converting via Imagick, no issue, totally approved.
Converting with this tool is going to be a big nope.

With regard to Imagick failing:
ImageMagick/ImageMagick#7426 (comment)
It seems that dependencies needs to be updated mostly.

@nbalkandzhiyski
Copy link
Contributor Author

@ildyria
Hi,
Thank you for your response.
I suspected we'll have issues with the fallback package.

I'll explore the possible solutions for libhief.
Currently the issue seems indeed to be related with libheif and not ImageMagick itself.

libheif version greater than 1.15 is not able to get installed on Debian versions <= 12 or Ubuntu <= 25.04 out of the box and as from your comment the possible solution will be to update libheif to >= 1.18.2

I'll update the PR once I have a resolution regarding this and update the docs if needed

Regards,

@ildyria
Copy link
Member

ildyria commented Jan 8, 2026

libheif version greater than 1.15 is not able to get installed on Debian versions <= 12 or Ubuntu <= 25.04 out of the box and as from your comment the possible solution will be to update libheif to >= 1.18.2

Yeah, but Debian versions <= 12 are no longer considered. 😃
We consider only Debian stable which is Trixie - aka version 13! 🥳

https://tracker.debian.org/pkg/libheif
stable: 1.19.8-1

Ubuntu is not in the scope of Lychee as our base images are on alpine or debian stable.

What I would do instead is to see if we can have a check in diagnostics for the version of libheif, and display a warning if it is too old. That way we do not have to deal with the fallback package. 👍

@ildyria
Copy link
Member

ildyria commented Jan 14, 2026

@nbalkandzhiyski I worked on your PR and simplified it. The code is 99% here, however I am not able to push to your repo on that specific branch. :/

I you can either unlock it or merge the branch feature/... from Lychee into your PR. then I will be able to merge it.
(I still need to have a look at the foreign constraint issue)

@ildyria ildyria merged commit 5f0cd2f into LycheeOrg:master Jan 14, 2026
43 checks passed
@ildyria ildyria deleted the feature/#298-HEIC-HEIF branch January 14, 2026 17:27
@codecov
Copy link

codecov bot commented Jan 14, 2026

Codecov Report

❌ Patch coverage is 85.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.41%. Comparing base (236d657) to head (9b5c877).
⚠️ Report is 1 commits behind head on master.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants